﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;
using SharpObjects.Cryptography;
using SharpObjects.Interfaces;
using SharpObjects.PathMapping;
using SharpObjects.Instantiation;

namespace SharpObjects.Configuration.ConfigurationProviders
{
    public class XmlConfigurationProvider : IConfigurationProvider, IXmlInitializable
    {
        private static class Tags
        {
            public const string Id = "id";
            public const string Version = "version";
            public const string SubVersion = "sub_version";
            public const string Modification = "modification";
            public const string ConfigurationVersion = "config_version";
            public const string ConfigurationSubVersion = "config_sub_version";
            public const string DefaultVersion = "default";
            public const string Assembly = "assembly";
            public const string Type = "type";
            public const string TypeMap = "configuration_type_map";
            public const string Fileref = "fileref";
            public const string Pathref = "pathref";
            public const string FileName = "file";
        }

        #region NESTED CLASSES

        public class TypeMap
        {
            public class MapEntry
            {
                public string TagName { get; set; }
                public string TypeName { get; set; }
                public string AssemblyName { get; set; }


                public MapEntry()
                {
                    this.TagName = null;
                    this.TypeName = null;
                    this.AssemblyName = null;
                }

                // <my_class_tag assembly = "..." type = "..."/> 
                public MapEntry(XmlElement initializationElement)
                {
                    this.TagName = initializationElement.Name;

                    if (initializationElement.HasAttribute(XmlConfigurationProvider.Tags.Assembly))
                    {
                        this.AssemblyName = initializationElement.GetAttribute(XmlConfigurationProvider.Tags.Assembly);
                    }

                    if (initializationElement.HasAttribute(XmlConfigurationProvider.Tags.Type))
                    {
                        this.TypeName = initializationElement.GetAttribute(XmlConfigurationProvider.Tags.Type);
                    }

                    if (this.AssemblyName == null || this.TypeName == null)
                    {
                        throw new ApplicationException("TypeMap entry is missing an assembly or type information: " + XmlUtil.CalculateXPath((XmlNode)initializationElement));
                    }
                }

            }


            public Dictionary<string, MapEntry> Map { get; set; }

            public TypeMap()
            {
                this.Map = new Dictionary<string, MapEntry>();
            }

            public TypeMap(XmlElement initializationElement)
            {
                this.Map = new Dictionary<string, MapEntry>();

                foreach (XmlNode node in initializationElement.ChildNodes)
                {
                    if (node is XmlElement)
                    {
                        XmlElement element = (XmlElement)node;
                        MapEntry entry = new MapEntry(element);
                        this.Map.Add(entry.TagName, entry);
                    }
                }
            }

        }

        public static class XmlUtil
        {
            public static string CalculateXPath(XmlNode node)
            {
                List<string> xpathElements = new List<string>();
                CalculateXPath(xpathElements, node);

                StringBuilder sb = new StringBuilder();

                for (int k = xpathElements.Count - 1; k >= 0; k--)
                {
                    sb.Append('/');
                    sb.Append(xpathElements[k]);
                }

                return sb.ToString();
            }

            private static void CalculateXPath(List<string> xpathElements, XmlNode node)
            {
                if (node is XmlAttribute)
                {
                    xpathElements.Add(string.Format("@{0}", node.Name));
                }
                else
                {
                    xpathElements.Add(node.Name);
                }

                XmlNode parent = node.SelectSingleNode("parent::*");

                if (parent != null && !(parent is XmlDocument))  // node.ParentNode.Name != "#document")
                {
                    CalculateXPath(xpathElements, parent);
                }
            }

            private static List<XmlNode> GetNodes(XmlElement root, string path)
            {
                XmlNodeList nodes = root.SelectNodes(path);

                List<XmlNode> nodeList = new List<XmlNode>();

                foreach (XmlNode node in nodes)
                {
                    nodeList.Add(node);
                }

                return nodeList;
            }

            //********************************************************************
            public static XmlElement CreateElement(XmlElement root, string sXpath)
            {
                string[] path = sXpath.Split('/');

                if (sXpath.StartsWith("//"))
                {
                    if (path.Length > 2)
                    {
                        XmlNode node = root.SelectSingleNode("//" + path[2]);

                        if (node != null)
                        {
                            if (node is XmlElement)
                            {
                                if (path.Length > 2)
                                {
                                    return CreateElement((XmlElement)node, path, 3);
                                }
                                else
                                {
                                    return node as XmlElement;
                                }
                            }
                            else
                            {
                                throw new Exception(string.Format("XPATH: '{0}' must point to an XmlElement", sXpath));
                            }
                        }
                        else
                        {
                            throw new Exception(string.Format("XmlElement not found: '//{0}'", path[2]));
                        }
                    }
                    else
                    {
                        throw new Exception(string.Format("Invalid XPATH: '{0}'", sXpath));
                    }
                }
                else if (sXpath.StartsWith("/"))
                {
                    if (path.Length < 2)
                    {
                        throw new Exception(string.Format("Invalid XPATH: '{0}'", sXpath));
                    }
                    else
                    {
                        if (path[1] != root.Name)
                        {
                            throw new Exception(string.Format("Invalid name of the root node: '{0}'", sXpath));
                        }

                        return CreateElement(root as XmlElement, path, 2);
                    }
                }
                else
                {
                    if (path.Length > 0)
                    {
                        XmlNode node = root.SelectSingleNode(path[0]);

                        if (node != null && node is XmlElement)
                        {
                            if (path.Length > 1)
                            {
                                return CreateElement(node as XmlElement, path, 1);
                            }
                            else
                            {
                                return node as XmlElement;
                            }
                        }
                        else
                        {
                            throw new Exception(string.Format("Invalid XPATH: '{0}'", path[0]));
                        }
                    }
                    else
                    {
                        throw new Exception(string.Format("Invalid XPATH: '{0}'", sXpath));
                    }
                }
            }

            private static XmlElement CreateElement(XmlElement parent, string[] path, int ind)
            {
                XmlNode node = parent.SelectSingleNode(path[ind]);

                if (node != null)
                {
                    if (node is XmlElement)
                    {
                        if ((path.Length - ind) > 1)
                        {
                            return CreateElement((XmlElement)node, path, ind + 1);
                        }
                        else
                        {
                            return node as XmlElement;
                        }
                    }
                    else
                    {
                        XmlElement elem = node.OwnerDocument.CreateElement(path[ind]);
                        parent.AppendChild(elem);

                        if ((path.Length - ind) > 1)
                        {
                            return CreateElement(elem, path, ind + 1);
                        }
                        else
                        {
                            return elem;
                        }
                    }
                }
                else
                {
                    XmlElement elem = parent.OwnerDocument.CreateElement(path[ind]);
                    parent.AppendChild(elem);

                    if ((path.Length - ind) > 1)
                    {
                        return CreateElement(elem, path, ind + 1);
                    }
                    else
                    {
                        return elem;
                    }
                }
            }

        }

        public static class CDataUtil
        {

            private static readonly char[] XmlIllegalChars = new char[] { '<', '>', '&', '"', '\'', '/' };


            public static string CreateCDataString(string value)
            {
                return CreateCDataString(value, true);
            }

            public static string CreateCDataString(string value, bool bOptional)
            {
                if (bOptional)
                {
                    if (value.IndexOfAny(XmlIllegalChars) >= 0)
                    {
                        return string.Format("<![CDATA[{0}]]>", value);
                    }
                    else
                    {
                        return value;
                    }
                }
                else
                {
                    return string.Format("<![CDATA[{0}]]>", value);
                }
            }

            public static XmlNode CreateCDataNode(XmlDocument doc, string value)
            {
                return CreateCDataNode(doc, value, true);
            }

            public static XmlNode CreateCDataNode(XmlDocument doc, string value, bool isOptional)
            {
                if (isOptional)
                {
                    if (value.IndexOfAny(XmlIllegalChars) >= 0)
                    {
                        XmlCDataSection cdata = doc.CreateCDataSection(value);
                        return cdata;
                    }
                    else
                    {
                        XmlText text = doc.CreateTextNode(value);
                        return text;
                    }
                }
                else
                {
                    XmlCDataSection cdata = doc.CreateCDataSection(value);
                    return cdata;
                }
            }
        }

        #endregion //NESTED CLASSES


        private XmlElement root = null;
        private string fileName = null;
        private IPathMapper pathMapper;
        private IInstanceFactory instanceFactory;
        private XmlCryptography xmlCryptography;
        private string _configurationVersion = Tags.DefaultVersion;
        private string _configurationSubVersion = Tags.DefaultVersion;
        private bool modified = false;

        private List<TypeMap> typeMaps = null;

        #region Constructors

        public XmlConfigurationProvider(IPathMapper pathMapper = null)
        {
            InitializeFromPathMapper(pathMapper);
            InitializeTypeMaps();
        }

        public XmlConfigurationProvider(string fileName, IPathMapper pathMapper = null)
        {
            InitializeFromPathMapper(pathMapper);
            InitializeFromFile(fileName);
        }

        public XmlConfigurationProvider(XmlElement root)
        {
            InitializeFromPathMapper(null);
            InitializeFromXmlElement(root);
            InitializeTypeMaps();
        }

        public XmlConfigurationProvider(XmlElement root, IPathMapper pathMapper)
        {
            InitializeFromPathMapper(pathMapper);
            InitializeFromXmlElement(root);
            InitializeTypeMaps();
        }

        public XmlConfigurationProvider()
        {
            this.pathMapper = PathMapperFactory.PathMapper;
            this.instanceFactory = new InstanceFactory(PathMapperFactory.PathMapper);
        }

        #region IXmlInitializable Members

        public void Initialize(XmlElement initializationElement)
        {
            lock (this)
            {
                XmlConfigurationProvider init = new XmlConfigurationProvider(initializationElement);
                string fileName = init.GetString(Tags.FileName, null);

                if (fileName != null)
                {
                    InitializeFromFile(fileName);
                }
                else
                {
                    InitializeFromXmlElement(initializationElement);
                }

                this.IsInitialized = true;
            }
        }

        private bool isInitialized = false;

        public bool IsInitialized
        {
            get
            {
              lock(this)
              {
                  return this.isInitialized;
              }
            }
            set
            {
                lock(this)
                {
                    this.isInitialized = value;
                }
            }
        }


        #endregion

        // initializer methods

        private void InitializeTypeMaps()
        {
            if (this.root != null)
            {
                this.typeMaps = new List<TypeMap>();

                List<XmlNode> mapElements = this.GetNodes(string.Format("//{0}", Tags.TypeMap));

                foreach (XmlElement typeMapElement in mapElements)
                {
                    TypeMap typeMap = new TypeMap(typeMapElement);
                    this.typeMaps.Add(typeMap);
                }
            }
        }

        private void InitializeFromPathMapper(IPathMapper pathMapper = null)
        {
            if(pathMapper == null)
            {
                this.pathMapper = PathMapperFactory.PathMapper;
            }
            else
            {
                this.pathMapper = pathMapper;
            }

            this.instanceFactory = new InstanceFactory(this.pathMapper);

            this.xmlCryptography = new XmlCryptography(this.pathMapper);
            
        }

        private void InitializeFromFile(string fileName)
        {
            this.fileName = this.pathMapper.Resolve(fileName);

            this.pathMapper.ConfigurationRootPath = Path.GetDirectoryName(this.fileName);

            XmlDocument document = Load(this.fileName);
            this.root = document.DocumentElement;

            this.xmlCryptography = new XmlCryptography(this.root, this.pathMapper);

            InitializeVersion();
            InitializeTypeMaps();
        }

        private void InitializeFromXmlElement(XmlElement root)
        {
            this.root = root;
            this.fileName = null;

            this.xmlCryptography = new XmlCryptography(this.root, this.pathMapper);

            InitializeVersion();
        }

        private void InitializeVersion()
        {
            XmlNode versionNode = this.root.SelectSingleNode("/*/@" + Tags.ConfigurationVersion);

            if(versionNode == null)
            {
                this._configurationVersion = Tags.DefaultVersion;
            }
            else
            {
                this._configurationVersion = versionNode.InnerText;
            }

            XmlNode subVersionNode = this.root.SelectSingleNode("/*/@" + Tags.ConfigurationSubVersion);

            if (subVersionNode == null)
            {
                this._configurationSubVersion = Tags.DefaultVersion;
            }
            else
            {
                this._configurationSubVersion = subVersionNode.InnerText;
            }

            this.SettingModification = Tags.DefaultVersion;
        }

        private static XmlDocument Load(string fileName)
        {
            XmlDocument document = new XmlDocument();

            if (!string.IsNullOrEmpty(fileName))
            {
                document.Load(fileName);
            }

            return document;
        }

        #endregion //Constructors 



        private string TagConfigurationRoot
        {
            get
            {
                if(this.root != null)
                {
                    return "/" + this.root.Name;
                }
                else
                {
                    return "/*";
                }
            }
        }

        public static TimeSpan MutexTimeout = TimeSpan.FromSeconds(30);

        private static void Save(XmlElement elem, string sFile)
        {
            string mutexId = Uri.EscapeUriString(sFile);

            Mutex mutex = new Mutex(false, mutexId);

            try
            {
                bool acquired = mutex.WaitOne(MutexTimeout);

                if (!acquired)
                {
                    throw new ApplicationException(string.Format("Mutex '{0}' could not be acquired - timeout", mutexId));
                }

                _Save(elem, sFile);
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }

        private static void _Save(XmlElement elem, string sFile)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(elem.OuterXml);

            XmlTextWriter w = null;

            try
            {
                w = new XmlTextWriter(sFile, new UTF8Encoding());

                w.Namespaces = true;
                w.Formatting = Formatting.Indented;
                w.Indentation = 4;
                w.IndentChar = ' ';

                doc.Save(w);
            }
            finally
            {
                if (w != null)
                {
                    w.Close();
                }
            }
        }



        private string GetNodeStringValue(XmlNode node)
        {
            if(node is XmlElement)
            {
                XmlElement element = (XmlElement) node;
                string value = this.xmlCryptography.GetDecryptedXmlElementValue(element, ref this.modified);

                if(this.Modified)
                {
                    this.Save();
                }

                return value;
            }
            else if(node is XmlAttribute)
            {
                XmlAttribute attribute = (XmlAttribute) node;
                return attribute.Value;
            }
            else
            {
                throw new ArgumentException(string.Format("XML node at the path '{0}' msut be an initializer or an attribute", XmlUtil.CalculateXPath(node)));
            }
        }

        private void SetNodeStringValue(XmlNode node, string value)
        {
            if(node is XmlElement)
            {
                XmlElement element = (XmlElement) node;
                element.InnerText = string.Empty;
                XmlNode child = CDataUtil.CreateCDataNode(node.OwnerDocument, value);
                element.AppendChild(child);
                this.xmlCryptography.EncryptXmlElementValue(element, ref this.modified);
            }
            else if(node is XmlAttribute)
            {
                XmlAttribute attribute = (XmlAttribute) node;
                attribute.Value = value;
            }
            else
            {
                throw new ArgumentException(string.Format("XML node at the path '{0}' msut be an initializer or an attribute", XmlUtil.CalculateXPath(node)));
            }
        }

        private XmlElement AddStringElement(string parentPath, string name, string value, string encryptionKeyName = null, string id = null)
        {
            XmlNode parentNode = GetNode(parentPath);

            if (parentNode == null)
            {
                parentNode = XmlUtil.CreateElement(this.root, parentPath);
            }

            if (parentNode is XmlElement)
            {
                XmlElement parent = (XmlElement) parentNode;
                XmlElement child = parent.OwnerDocument.CreateElement(name);
                parent.AppendChild(child);

                XmlNode cdata = CDataUtil.CreateCDataNode(parentNode.OwnerDocument, value);
                child.AppendChild(cdata);

                if(id != null)
                {
                    child.SetAttribute(Tags.Id, id);
                }

                if(encryptionKeyName != null)
                {
                    child.SetAttribute(XmlCryptography.Tags.encryptionKeyReference, encryptionKeyName);
                    this.xmlCryptography.EncryptXmlElementValue(child, ref this.modified);
                }

                return child;
            }
            else
            {
                throw new ArgumentException(string.Format("XML node '{0}' must be an initializer", parentPath));
            }
        }

        private void AddStringElements(string parentPath, string name, string [] values, string encryptionKeyName = null, string [] ids = null)
        {
            XmlNode parentNode = GetNode(parentPath);

            if(parentNode == null)
            {
                parentNode = XmlUtil.CreateElement(this.root, parentPath);
            }

            if (parentNode is XmlElement)
            {
                XmlElement parent = (XmlElement)parentNode;

                for (int k = 0; k < values.Length; k++)
                {
                    XmlElement child = parent.OwnerDocument.CreateElement(name);
                    parent.AppendChild(child);

                    XmlNode cdata = CDataUtil.CreateCDataNode(parentNode.OwnerDocument, values[k]);
                    child.AppendChild(cdata);

                    if (ids != null && k < (ids.Length - 1))
                    {
                        child.SetAttribute(Tags.Id, ids[k]);
                    }

                    if (encryptionKeyName != null)
                    {
                        child.SetAttribute(XmlCryptography.Tags.encryptionKeyReference, encryptionKeyName);
                        this.xmlCryptography.EncryptXmlElementValue(child, ref this.modified);
                    }
                }
            }
            else
            {
                throw new ArgumentException(string.Format("XML node '{0}' must be an initializer", parentPath));
            }
        }

        private XmlNode GetNode(string path, string settingModification = Tags.DefaultVersion)
        {
            XmlNodeList nodes = this.root.SelectNodes(path);
            XmlNode foundNode = null;
            XmlNode defaultNode = null;

            if (nodes == null)
            {
                return null;
            }

            foreach (XmlNode node in nodes)
            {
                if (node is XmlElement)
                {
                    XmlElement element = (XmlElement)node;

                    if (element.HasAttribute(Tags.Version))
                    {
                        string elementVersion = element.GetAttribute(Tags.Version);

                        if (elementVersion == Tags.DefaultVersion)
                        {
                            defaultNode = node;
                        }

                        if (elementVersion == this.ConfigurationVersion)
                        {
                            foundNode = node;
                            break;
                        }
                    }
                    else
                    {
                        if (defaultNode == null)
                        {
                            defaultNode = node;
                        }
                    }
                }
                else
                {
                    foundNode = node;
                    break;
                }
            }

            if (foundNode == null)
            {
                if (defaultNode == null)
                {
                    return null;
                }
                else
                {
                    foundNode = defaultNode;
                }
            }

            return GetReferenceNode(foundNode);
        }

        public bool HasSetting(string path)
        {
            return GetNode(path) != null;
        }

        private XmlNode GetNode(string path)
        {
            XmlNodeList nodes = this.root.SelectNodes(path);
            XmlNode foundNode = null;
            XmlNode defaultNode = null;

            if(nodes == null)
            {
                return null;
            }

            foreach(XmlNode node in nodes)
            {
                if(node is XmlElement)
                {
                    XmlElement element = (XmlElement) node;

                    if(element.HasAttribute(Tags.Version))
                    {
                        string elementVersion = element.GetAttribute(Tags.Version);

                        if(elementVersion == Tags.DefaultVersion)
                        {
                            defaultNode = node;
                        }

                        if(elementVersion == this.ConfigurationVersion)
                        {
                            foundNode = node;
                            break;
                        }
                    }
                    else
                    {
                        if (defaultNode == null)
                        {
                            defaultNode = node;
                        }
                    }
                }
                else
                {
                    foundNode = node;
                    break;
                }
            }

            if(foundNode == null)
            {
                if(defaultNode == null)
                {
                    return null;
                }
                else
                {
                    foundNode = defaultNode;
                }
            }

            return GetReferenceNode(foundNode);
        }


        private List<XmlNode> GetNodes(string path)
        {
            XmlNodeList nodes = this.root.SelectNodes(path);
            List<XmlNode> nodeList = new List<XmlNode>();
            List<XmlNode> defaultNodeList = new List<XmlNode>();

            if (nodes == null)
            {
                return nodeList;
            }

            foreach (XmlNode node in nodes)
            {
                if (node is XmlElement)
                {
                    XmlElement element = (XmlElement) node;

                    if (element.HasAttribute(Tags.Version))
                    {
                        string elementVersion = element.GetAttribute(Tags.Version);

                        if (elementVersion == Tags.DefaultVersion)
                        {
                            defaultNodeList.Add(node);
                        }

                        if (elementVersion == this.ConfigurationVersion)
                        {
                            nodeList.Add(node);
                        }
                    }
                    else
                    {
                        defaultNodeList.Add(node);
                    }
                }
                else
                {
                    nodeList.Add(node);
                }
            }

            nodeList.AddRange(defaultNodeList);

            for (int k = 0; k < nodeList.Count; k++)
            {
                nodeList[k] = GetReferenceNode(nodeList[k]);
            }

            return nodeList;
        }

        private int ConvertToInt(string value)
        {
            if (value.StartsWith("0x"))
            {
                return Convert.ToInt32(value.Substring(2), 16);
            }
            else
            {
                return Convert.ToInt32(value, 10);
            }
        }

        private long ConvertToLong(string value)
        {
            if (value.StartsWith("0x"))
            {
                return Convert.ToInt64(value.Substring(2), 16);
            }
            else
            {
                return Convert.ToInt64(value, 10);
            }
        }


        #region IConfigurationProvider Members

        public string Source
        {
            get
            {
                return this.fileName;
            }
            set
            {
                //TODO should it be MapConfigurationFilePath() ?
                this.fileName = this.pathMapper.Resolve(value);
            }
        }

        public bool Modified
        {
            get
            {
                lock (this)
                {
                    return this.modified;
                }
            }
            set
            {
                lock (this)
                {
                    this.modified = value;
                }
            }
        }

        public string ConfigurationVersion
        {
            get
            {
                return this._configurationVersion;
            }
            set
            {
                this._configurationVersion = value;
                this.root.SetAttribute(Tags.ConfigurationVersion, this._configurationVersion);
            }
        }

        public string ConfigurationSubVersion
        {
            get
            {
                return this._configurationSubVersion;
            }
            set
            {
                this._configurationSubVersion = value;
                this.root.SetAttribute(Tags.ConfigurationSubVersion, this._configurationSubVersion);
            }
        }

        public string SettingModification { get; set; }

        public void SetValue<T>(string path, T value)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                SetNodeStringValue(node, value.ToString());
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public void AddValue<T>(string parentPath, string name, T value, string encryptionKeyName = null, string id = null)
        {
            AddStringElement(parentPath, name, value.ToString(), encryptionKeyName, id);
        }

        public void AddValues<T>(string parentPath, string name, T[] values, string encryptionKeyName = null, string[] ids = null)
        {
            string[] stringValues = new string[values.Length];

            for (int k = 0; k < values.Length; k++)
            {
                stringValues[k] = values[k].ToString();
            }

            AddStringElements(parentPath, name, stringValues, encryptionKeyName, ids);
        }


        public bool GetBoolean(string path)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return Convert.ToBoolean(value);
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public bool GetBoolean(string path, bool defaultValue)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return Convert.ToBoolean(value);
            }
            else
            {
                return defaultValue;
            }
        }

        public bool[] GetBooleans(string path)
        {
            List<XmlNode> nodes = GetNodes(path);
            bool [] values = new bool[nodes.Count];

            for (int k = 0; k < nodes.Count; k++ )
            {
                string value = GetNodeStringValue(nodes[k]);
                values[k] = Convert.ToBoolean(value);
            }

            return values;
        }


        public int GetInteger(string path)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return ConvertToInt(value);
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public int GetInteger(string path, int defaultValue)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return ConvertToInt(value);
            }
            else
            {
                return defaultValue;
            }
        }

        public int[] GetIntegers(string path)
        {
            List<XmlNode> nodes = GetNodes(path);
            int[] values = new int[nodes.Count];

            for (int k = 0; k < nodes.Count; k++)
            {
                string value = GetNodeStringValue(nodes[k]);
                values[k] = ConvertToInt(value);
            }

            return values;
        }

        public long GetLong(string path)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return ConvertToLong(value);
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public long GetLong(string path, long defaultValue)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return ConvertToLong(value);
            }
            else
            {
                return defaultValue;
            }
        }

        public long[] GetLongs(string path)
        {
            List<XmlNode> nodes = GetNodes(path);
            long[] values = new long[nodes.Count];

            for (int k = 0; k < nodes.Count; k++)
            {
                string value = GetNodeStringValue(nodes[k]);
                values[k] = ConvertToLong(value);
            }

            return values;
        }

        public double GetDouble(string path)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return Convert.ToDouble(value);
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public double GetDouble(string path, double defaultValue)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return Convert.ToDouble(value);
            }
            else
            {
                return defaultValue;
            }
        }

        public double[] GetDoubles(string path)
        {
            List<XmlNode> nodes = GetNodes(path);
            double[] values = new double[nodes.Count];

            for (int k = 0; k < nodes.Count; k++)
            {
                string value = GetNodeStringValue(nodes[k]);
                values[k] = Convert.ToDouble(value);
            }

            return values;
        }

        public string GetString(string path)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return value;
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public string GetString(string path, string defaultValue)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return value;
            }
            else
            {
                return defaultValue;
            }
        }

        public string[] GetStrings(string path)
        {
            List<XmlNode> nodes = GetNodes(path);
            string[] values = new string[nodes.Count];

            for (int k = 0; k < nodes.Count; k++)
            {
                string value = GetNodeStringValue(nodes[k]);
                values[k] = value;
            }

            return values;
        }

        public TEnum GetEnum<TEnum>(string path)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return (TEnum)Enum.Parse(typeof(TEnum), value, true);
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public TEnum GetEnum<TEnum>(string path, TEnum defaultValue)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return (TEnum)Enum.Parse(typeof(TEnum), value, true);
            }
            else
            {
                return defaultValue;
            }
        }

        public TEnum[] GetEnums<TEnum>(string path)
        {
            List<XmlNode> nodes = GetNodes(path);
            TEnum[] values = new TEnum[nodes.Count];

            for (int k = 0; k < nodes.Count; k++)
            {
                string value = GetNodeStringValue(nodes[k]);
                values[k] = (TEnum)Enum.Parse(typeof(TEnum), value, true);
            }

            return values;
        }


        public DateTime GetDateTime(string path)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return Convert.ToDateTime(value);
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public DateTime GetDateTime(string path, DateTime defaultValue)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                string value = GetNodeStringValue(node);
                return Convert.ToDateTime(value);
            }
            else
            {
                return defaultValue;
            }
        }

        public DateTime[] GetDateTimes(string path)
        {
            List<XmlNode> nodes = GetNodes(path);
            DateTime[] values = new DateTime[nodes.Count];

            for (int k = 0; k < nodes.Count; k++)
            {
                string value = GetNodeStringValue(nodes[k]);
                values[k] = Convert.ToDateTime(value);
            }

            return values;
        }

        public T GetObject<T>(string path)
        {
            return GetAndInitializeObject<T>(path, null);
        }

        public T GetObject<T>(string path, T defaultValue)
        {
            return GetAndInitializeObject<T>(path, defaultValue, null);
        }

        public T GetObject<T>(string path, ITypeDescriptor typeInfo)
        {
            return GetAndInitializeObject<T>(path, typeInfo, null);
        }

        public T GetObject<T>(string path, ITypeDescriptor typeInfo, T defaultValue)
        {
            return GetAndInitializeObject<T>(path, typeInfo, defaultValue, null);
        }

        public T[] GetObjects<T>(string path)
        {
            return GetAndConstructObjects<T>(path, null);
        }

        public T[] GetObjects<T>(string path, ITypeDescriptor typeInfo)
        {
            return GetAndConstructObjects<T>(path, typeInfo, null);
        }

        public T GetAndInitializeObject<T>(string path, IInitializer<T> initializer)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                if (node is XmlElement)
                {
                    XmlElement element = (XmlElement) node;
                    return CreateObjectFromXmlElement<T>(element, initializer);
                }
                else
                {
                    throw new ArgumentException(string.Format("XML node a the XPATH '{0}' must be an XML element", path));
                }
            }
            else
            {
                throw new ArgumentException("No setting found at XPATH: " + path);
            }
        }

        public T GetAndInitializeObject<T>(string path, T defaultValue, IInitializer<T> initializer)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                if (node is XmlElement)
                {
                    XmlElement element = (XmlElement)node;
                    return CreateObjectFromXmlElement<T>(element, initializer);
                }
                else
                {
                    throw new ArgumentException(string.Format("XML node a the XPATH '{0}' must be an XML element", path));
                }
            }
            else
            {
                return defaultValue;
            }
        }

        public T GetAndInitializeObject<T>(string path, ITypeDescriptor typeInfo, IInitializer<T> initializer)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                if (node is XmlElement)
                {
                    XmlElement element = (XmlElement) node;
                    ITypeDescriptor typeDescriptor = new TypeDescriptor(typeInfo.AssemblyPath, typeInfo.TypeName);
                    T obj = this.instanceFactory.CreateObject<T>(typeDescriptor);

                    if(obj is IXmlInitializable)
                    {
                        IXmlInitializable iobject = (IXmlInitializable) obj;

                        if (!iobject.IsInitialized)
                        {
                            iobject.Initialize(element);
                        }
                    }

                    if(initializer != null)
                    {
                        initializer.Initialize(obj);
                    }

                    return obj;
                }
                else
                {
                    throw new ArgumentException(string.Format("XML node at the path '{0}' msut be an initializer", XmlUtil.CalculateXPath(node)));
                }
            }
            else
            {
                throw new ArgumentException(string.Format("Configuration setting is not found: '{0}'", path));
            }
        }

        public T GetAndInitializeObject<T>(string path, ITypeDescriptor typeInfo, T defaultValue, IInitializer<T> initializer)
        {
            XmlNode node = GetNode(path);

            if (node != null)
            {
                if (node is XmlElement)
                {
                    XmlElement element = (XmlElement)node;
                    ITypeDescriptor typeDescriptor = new TypeDescriptor(typeInfo.AssemblyPath, typeInfo.TypeName);
                    T obj = this.instanceFactory.CreateObject<T>(typeDescriptor);

                    if (obj is IXmlInitializable)
                    {
                        IXmlInitializable iobject = (IXmlInitializable)obj;

                        if (!iobject.IsInitialized)
                        {
                            iobject.Initialize(element);
                        }
                    }

                    if (initializer != null)
                    {
                        initializer.Initialize(obj);
                    }

                    return obj;
                }
                else
                {
                    throw new ArgumentException(string.Format("XML node at the path '{0}' msut be an initializer", XmlUtil.CalculateXPath(node)));
                }
            }
            else
            {
                return defaultValue;
            }
        }

        public T[] GetAndConstructObjects<T>(string path, IInitializer<T> initializer)
        {
            List<XmlNode> nodes = GetNodes(path);
            T[] values = new T[nodes.Count];

            for (int k = 0; k < nodes.Count; k++)
            {
                if (nodes[k] is XmlElement)
                {
                    XmlElement element = (XmlElement)nodes[k];
                    values[k] = CreateObjectFromXmlElement<T>(element, initializer);
                }
                else
                {
                    throw new ArgumentException(string.Format("XML node at the path '{0}' msut be an initializer", XmlUtil.CalculateXPath(nodes[k])));
                }
            }

            return values;
        }

        public T[] GetAndConstructObjects<T>(string path, ITypeDescriptor typeInfo, IInitializer<T> initializer)
        {
            List<XmlNode> nodes = GetNodes(path);
            T[] values = new T[nodes.Count];

            for (int k = 0; k < nodes.Count; k++)
            {
                if (nodes[k] is XmlElement)
                {
                    XmlElement element = (XmlElement)nodes[k];
                    ITypeDescriptor typeDescriptor = new TypeDescriptor(typeInfo.AssemblyPath, typeInfo.TypeName);
                    T obj = this.instanceFactory.CreateObject<T>(typeDescriptor);

                    if (obj is IXmlInitializable)
                    {
                        IXmlInitializable iobject = (IXmlInitializable)obj;

                        if (!iobject.IsInitialized)
                        {
                            iobject.Initialize(element);
                        }
                    }

                    if (initializer != null)
                    {
                        initializer.Initialize(obj);
                    }

                    values[k] = obj;
                }
                else
                {
                    throw new ArgumentException(string.Format("XML node at the path '{0}' msut be an initializer", XmlUtil.CalculateXPath(nodes[k])));
                }
            }

            return values;
        }



        public void AddEncryptionKey(string encryptionKeyName, string encryptionKeyFileName)
        {
            if (!this.xmlCryptography.Providers.ContainsKey(encryptionKeyName))
            {
                XmlElement keyElement = AddStringElement("/" + this.root.Name + "/" + XmlCryptography.Tags.encryptionKeys, XmlCryptography.Tags.encryptionKey, encryptionKeyFileName, null, encryptionKeyName);
                this.xmlCryptography = new XmlCryptography(this.root, this.pathMapper);
            }
        }

        /// <summary>
        /// Creates an encryption key file
        /// </summary>
        /// <param name="encryptionKeyFileName"></param>
        /// <param name="mapToAppData">When true and the path is relative - maps the path to the Application Data folder for the user</param>
        /// <param name="overwrite">Forces to overwrite existing key file</param>
        /// <returns>Returns full path to the created file</returns>
        public string CreateEncryptionKey(string encryptionKeyFileName, bool mapToAppData = false, bool overwrite = false)
        {
            return this.xmlCryptography.CreateCryptoKeyFile(encryptionKeyFileName, mapToAppData:mapToAppData, overwrite:overwrite);
        }

        public void EncryptSetting(string path, string encryptionKeyName)
        {
            XmlNode node = GetNode(path);

            if(node == null)
            {
                throw new ArgumentException(string.Format("XML node at the XPATH '{0}' is not found", path));
            }

            if(! (node is XmlElement))
            {
                throw new ArgumentException(string.Format("XML node at the XPATH '{0}' must be an XML initializer", path));
            }

            if(!this.xmlCryptography.Providers.ContainsKey(encryptionKeyName))
            {
                throw new ArgumentException(string.Format("Unknown encryption key: '{0}'", encryptionKeyName));
            }

            XmlElement element = (XmlElement) node;

            this.xmlCryptography.EncryptXmlElementValue(element, ref this.modified);

            if (this.Modified)
            {
                this.Save();
            }
        }

        public void RemoveEncryption(string path)
        {
            XmlNode node = GetNode(path);

            if (node == null)
            {
                throw new ArgumentException(string.Format("XML node at the XPATH '{0}' is not found", path));
            }

            if (!(node is XmlElement))
            {
                throw new ArgumentException(string.Format("XML node at the XPATH '{0}' must be an XML initializer", path));
            }

            XmlElement element = (XmlElement)node;


            lock (this)
            {
                this.xmlCryptography.RemoveXmlElementValueEncryption(element, ref this.modified);
            }

            if(this.Modified)
            {
                this.Save();
            }
        }

        public void Save()
        {
            lock (this)
            {
                if (this.fileName != null)
                {
                    Save(this.root.OwnerDocument.DocumentElement, this.fileName);
                    this.modified = false;
                }
            }
        }

        #endregion

        #region XML Utils

        private T CreateObjectFromXmlFile<T>(string file, string xpath, IInitializer<T> initializer = null)
        {
            file = pathMapper.MapConfigurationFilePath(file);
            XmlDocument document = new XmlDocument();
            document.Load(file);
            return CreateObjectFromXmlDocument<T>(document, xpath, initializer);
        }

        private T CreateObjectFromXmlDocument<T>(XmlDocument document, string xpath, IInitializer<T> initializer = null)
        {
            XmlNode node = document.DocumentElement.SelectSingleNode(xpath);

            if (node == null)
            {
                throw new ApplicationException(string.Format("The XPATH '{0}' must evaluate to an XML node", xpath));
            }

            XmlElement element = node as XmlElement;

            if (element == null)
            {
                throw new ApplicationException(string.Format("The XPATH '{0}' must evaluate to an XML initializer", xpath));
            }

            return CreateObjectFromXmlElement<T>(element, initializer);
        }

        private T CreateObjectFromXmlElement<T>(XmlElement element, IInitializer<T> initializer = null)
        {
            ITypeDescriptor typeDescriptor = GetTypeDescriptor(element);
            T obj = this.instanceFactory.CreateObject<T>(typeDescriptor);

            if (obj is IXmlInitializable)
            {
                IXmlInitializable iobject = (IXmlInitializable)obj;

                if (!iobject.IsInitialized)
                {
                    iobject.Initialize(element);
                }
            }

            if(initializer != null)
            {
                initializer.Initialize(obj);
            }

            return obj;
        }


        private ITypeDescriptor GetTypeDescriptor(XmlElement element)
        {
            ITypeDescriptor typeDescriptor = new TypeDescriptor();

            if (element.HasAttribute(Tags.Assembly))
            {
                typeDescriptor.AssemblyPath = element.GetAttribute(Tags.Assembly);
            }
            else
            {
                XmlNode node = element.SelectSingleNode(Tags.Assembly);

                if (node != null)
                {
                    typeDescriptor.AssemblyPath = node.InnerText;
                }
            }

            if (element.HasAttribute(Tags.Type))
            {
                typeDescriptor.TypeName = element.GetAttribute(Tags.Type);
            }
            else
            {
                XmlNode node = element.SelectSingleNode(Tags.Type);

                if (node != null)
                {
                    typeDescriptor.TypeName = node.InnerText;
                }
            }

            if (typeDescriptor.AssemblyPath == null || typeDescriptor.TypeName == null)
            {
                if (typeDescriptor.AssemblyPath != null)
                {
                    typeDescriptor.AssemblyPath = pathMapper.MapAssemblyBasePath(typeDescriptor.AssemblyPath);
                }

                if(this.typeMaps == null)
                {
                    InitializeTypeMaps();
                }

                if(this.typeMaps != null)
                {
                    foreach(TypeMap typeMap in this.typeMaps)
                    {
                        if(typeMap.Map.ContainsKey(element.Name))
                        {
                            typeDescriptor.AssemblyPath = typeMap.Map[element.Name].AssemblyName;
                            typeDescriptor.TypeName = typeMap.Map[element.Name].TypeName;
                        }
                    }
                }
            }

            if (typeDescriptor.AssemblyPath == null || typeDescriptor.TypeName == null)
            {
                string xpath = XmlUtil.CalculateXPath(element);
                throw new ApplicationException(string.Format("XML root at XPATH '{0}' must have both '{1}' and '{2}' specified either as attributes or sub-elements", xpath, Tags.Assembly, Tags.Type));
            }

            return typeDescriptor;
        }

        private XmlNode GetReferenceNode(XmlNode node)
        {
            if (node != null && node is XmlElement)
            {
                XmlElement elem = (XmlElement)node;

                if (elem.HasAttribute(Tags.Fileref))
                {
                    if (elem.HasAttribute(Tags.Pathref))
                    {
                        string fileName = this.pathMapper.Resolve(elem.GetAttribute(Tags.Fileref));
                        fileName = this.pathMapper.MapConfigurationFilePath(fileName);
                        string path = elem.GetAttribute(Tags.Pathref);

                        XmlDocument doc = new XmlDocument();
                        doc.Load(fileName);

                        node = doc.SelectSingleNode(path);
                        node = GetReferenceNode(node);
                    }
                }
            }

            return node;
        }
        #endregion //XML Utils


    }
}
